//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.server.handler;

import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Locale;

import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.statistic.CounterStatistic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Dumps GET and POST requests as HTML response content.
 * Useful for testing and debugging.
 * The following request query parameters are understood:<dl>
 *     <dt>flush</dt><dd>A boolean parameter, which if true causes the response to be flushed before the content is generated</dd>
 *     <dt>empty</dt><dd>A boolean parameter, which if true causes the response to be empty</dd>
 *     <dt>read</dt><dd>An integer parameter, which if set causes a specific number of bytes of content to be read, else the
 *                      entire content is read.</dd>
 *     <dt>date</dt><dd>A String parameter, which if set is used as the value of a Date header in the response</dd>
 *     <dt>ISE</dt><dd>A boolean parameter, which if true causes handling method to throw an {@link IllegalStateException}</dd>
 *     <dt>error</dt><dd>An integer parameter, which if set is used as the response status code.</dd>
 * </dl>
 */
@ManagedObject
public class DumpHandler extends Handler.Abstract
{
    private static final Logger LOG = LoggerFactory.getLogger(DumpHandler.class);

    private final Blocker.Shared _blocker = new Blocker.Shared();
    private final String _label;
    private final CounterStatistic _handled = new CounterStatistic();

    public DumpHandler()
    {
        this("Dump Handler");
    }

    public DumpHandler(String label)
    {
        _label = label;
    }

    @ManagedAttribute("handled count")
    public CounterStatistic getHandledCounter()
    {
        return _handled;
    }

    @Override
    public boolean handle(Request request, Response response, Callback callback) throws Exception
    {
        try
        {
            _handled.increment();
            if (LOG.isDebugEnabled())
                LOG.debug("dump {}", request);
            HttpURI httpURI = request.getHttpURI();

            Fields params = Request.extractQueryParameters(request);

            if (Boolean.parseBoolean(params.getValue("flush")))
            {
                try (Blocker.Callback blocker = _blocker.callback())
                {
                    response.write(false, null, blocker);
                    blocker.block();
                }
            }

            if (Boolean.parseBoolean(params.getValue("empty")))
            {
                response.setStatus(200);
                callback.succeeded();
                return true;
            }

            Utf8StringBuilder read = null;
            if (params.getValue("read") != null)
            {
                read = new Utf8StringBuilder();
                int len = Integer.parseInt(params.getValue("read"));
                byte[] buffer = new byte[8192];

                Content.Chunk chunk = null;
                while (len > 0)
                {
                    if (chunk == null)
                    {
                        chunk = request.read();
                        if (chunk == null)
                        {
                            try (Blocker.Runnable blocker = _blocker.runnable())
                            {
                                request.demand(blocker);
                                blocker.block();
                            }
                            continue;
                        }
                    }

                    if (Content.Chunk.isFailure(chunk))
                    {
                        callback.failed(chunk.getFailure());
                        return true;
                    }

                    int l = Math.min(buffer.length, Math.min(len, chunk.remaining()));
                    int r = chunk.get(buffer, 0, l);
                    read.append(buffer, 0, r);
                    len -= r;

                    if (!chunk.hasRemaining())
                    {
                        boolean last = chunk.isLast();
                        chunk.release();
                        chunk = null;
                        if (last)
                            break;
                    }
                }
                if (chunk != null)
                    chunk.release();
            }

            if (params.getValue("date") != null)
                response.getHeaders().put("Date", params.getValue("date"));

            if (params.getValue("ISE") != null)
                throw new IllegalStateException("Testing ISE");

            if (params.getValue("error") != null)
            {
                response.setStatus(Integer.parseInt(params.getValue("error")));
                callback.succeeded();
                return true;
            }

            response.getHeaders().put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.TEXT_HTML.asString());

            ByteArrayOutputStream buf = new ByteArrayOutputStream(2048);
            Writer writer = new OutputStreamWriter(buf, StandardCharsets.ISO_8859_1);
            writer.write("<html><h1>" + _label + "</h1>\n");
            writer.write("<pre>httpURI=" + httpURI + "</pre><br/>\n");
            writer.write("<pre>httpURI.path=" + httpURI.getPath() + "</pre><br/>\n");
            writer.write("<pre>httpURI.query=" + httpURI.getQuery() + "</pre><br/>\n");
            writer.write("<pre>httpURI.pathQuery=" + httpURI.getPathQuery() + "</pre><br/>\n");
            writer.write("<pre>locales=" + Request.getLocales(request).stream().map(Locale::toLanguageTag).toList() + "</pre><br/>\n");
            writer.write("<pre>pathInContext=" + Request.getPathInContext(request) + "</pre><br/>\n");
            writer.write("<pre>contentType=" + request.getHeaders().get(HttpHeader.CONTENT_TYPE) + "</pre><br/>\n");
            writer.write("<pre>servername=" + Request.getServerName(request) + "</pre><br/>\n");
            writer.write("<pre>local=" + Request.getLocalAddr(request) + ":" + Request.getLocalPort(request) + "</pre><br/>\n");
            writer.write("<pre>remote=" + Request.getRemoteAddr(request) + ":" + Request.getRemotePort(request) + "</pre><br/>\n");
            writer.write("<h3>Header:</h3><pre>");
            writer.write(String.format("%4s %s %s\n", request.getMethod(), httpURI.getPathQuery(), request.getConnectionMetaData().getProtocol()));
            for (HttpField field : request.getHeaders())
            {
                String name = field.getName();
                writer.write(name);
                writer.write(": ");
                String value = field.getValue();
                writer.write(value == null ? "" : value);
                writer.write("\n");
            }

            writer.write("</pre>\n<h3>Attributes:</h3>\n<pre>");
            for (String attr : request.getAttributeNameSet())
            {
                writer.write(attr);
                writer.write("=");
                writer.write(request.getAttribute(attr).toString());
                writer.write("\n");
            }

            writer.write("</pre>\n<h3>Content:</h3>\n<pre>");
            if (read != null)
                writer.write(read.toCompleteString());
            else
                writer.write(Content.Source.asString(request));

            writer.write("</pre>\n");
            writer.write("</html>\n");
            writer.flush();

            // commit now
            if (!Boolean.parseBoolean(params.getValue("no-content-length")))
                response.getHeaders().put(HttpHeader.CONTENT_LENGTH, buf.size() + 1000);

            response.getHeaders().add("Before-Flush", response.isCommitted() ? "Committed???" : "Not Committed");

            try (Blocker.Callback blocker = _blocker.callback())
            {
                response.write(false, BufferUtil.toBuffer(buf.toByteArray()), blocker);
                blocker.block();
            }
            response.getHeaders().add("After-Flush", "These headers should not be seen in the response!!!");
            String value = response.isCommitted() ? "Committed" : "Not Committed?";
            response.getHeaders().add("After-Flush", value);

            // write remaining content after commit
            String padding = "ABCDEFGHIJ".repeat(99) + "ABCDEFGH\r\n";

            try (Blocker.Callback blocker = _blocker.callback())
            {
                response.write(true, BufferUtil.toBuffer(padding.getBytes(StandardCharsets.ISO_8859_1)), blocker);
                blocker.block();
            }

            callback.succeeded();
            return true;
        }
        finally
        {
            _handled.decrement();
        }
    }
}
